/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package eu.xibit.jadardb;

import eu.xibit.jadardb.SpaceManager.Space;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * This is the main class of the JadarDB. You can access all the functionality through this class.
 *
 * @author dipacs
 */
public final class JadarDB {

    private static final class CachedValue implements Comparable<CachedValue> {

        private final String key;
        private final Object value;
        private transient long lastUsed;
        private transient long lastUsedNano;

        public CachedValue(String key, Object value) {
            this.key = key;
            this.value = value;
            this.lastUsed = System.currentTimeMillis();
            this.lastUsedNano = System.nanoTime();
        }

        public String getKey() {
            return key;
        }

        public Object getValue() {
            return value;
        }

        public long getLastUsed() {
            return lastUsed;
        }

        public void touch() {
            this.lastUsed = System.currentTimeMillis();
            this.lastUsedNano = System.nanoTime();
        }

        @Override
        public int compareTo(CachedValue o) {
            if (o == null) {
                return 1;
            }

            if (this.lastUsed < o.lastUsed) {
                return -1;
            } else if (this.lastUsed > o.lastUsed) {
                return 1;
            } else {
                if (this.lastUsedNano < o.lastUsedNano) {
                    return -1;
                } else if (this.lastUsedNano > o.lastUsedNano) {
                    return 1;
                } else {
                    return 0;
                }
            }
        }
    }

    public static final class Entity<T> {

        private final String key;
        private final T value;

        public Entity(String key, T value) {
            this.key = key;
            this.value = value;
        }

        public String getKey() {
            return key;
        }

        public T getValue() {
            return value;
        }
    }
    private final File dbFile;
    private volatile InMemoryFile memFile;
    private final SpaceManager spaceManager = new SpaceManager();
    private final HashMap<String, Long> keyCache = new HashMap<String, Long>();
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    
    
    

    /**
     * Opens a JadarDB specified in the dbFile parameter, or creates a new Database if it isn't exists.
     *
     * @param dbFile The file which opened or created as a JadarDB file.
     *
     * @throws FileNotFoundException If the given file can't be opened or created.
     * @throws IOException If any IO error occured during the opening mechanism.
     */
    public JadarDB(File dbFile) throws FileNotFoundException, IOException {
        this.dbFile = dbFile;
        this.memFile = new InMemoryFile(dbFile);
        init();
    }
    
    
    

    /**
     * Returns the dbFile. This is the same what you give in the constructor.
     *
     * @return The dbFile.
     */
    public File getDbFile() {
        return dbFile;
    }

    /**
     * Writes a the value in to the database with the specified key. If a data with this key is already exists than the
     * previous data will be removed.
     *
     * @param key The key of the data. You can use this to get the value later.
     * @param value The value which saved in to the database.
     */
    public void write(String key, Object value) {
        try {
            lock.writeLock().lock();
            if (key == null) {
                throw new NullPointerException("The key can not be null.");
            }
            if (value == null) {
                throw new NullPointerException("The value can not be null.");
            }

            if (keyCache.containsKey(key)) {
                delete(key);
            }

            ByteArrayOutputStream baos = null;
            ObjectOutputStream oos = null;
            try {
                baos = new ByteArrayOutputStream();
                int keyLen = key.length();
                baos.write((keyLen >>> 8) & 0xff);
                baos.write((keyLen) & 0xff);
                baos.write(key.getBytes("UTF-8"));
                oos = new ObjectOutputStream(baos);
                oos.writeObject(value);
                byte[] data = baos.toByteArray();
                long pointer = spaceManager.reserve(data.length + 16) + 11;

                // extending the file if needed
                if (memFile.length() <= pointer + data.length) {
                    memFile.setLength(memFile.length() + data.length + 16 + 1000000);
                }

                long next = spaceManager.getNext(pointer - 11);
                if (next > -1) {
                    next += 11;
                }
                memFile.writeLong(pointer, data.length + 16);
                memFile.writeLong(pointer + 8, next);
                memFile.write(pointer + 16, data);

                long prevPointer = spaceManager.getPrevious(pointer - 11);
                if (prevPointer > -1) {
                    prevPointer += 11;
                    memFile.writeLong(prevPointer + 8, pointer);
                } else {
                    memFile.writeLong(3, pointer);
                }

                keyCache.put(key, pointer);

                shrinkDatabase();
            } catch (IOException ex) {
                throw new RuntimeException(ex);
            } finally {
                if (oos != null) {
                    try {
                        oos.close();
                    } catch (IOException ex) {
                        // nothing to do
                    }
                }
                if (baos != null) {
                    try {
                        baos.close();
                    } catch (IOException ex) {
                        // nothing to do
                    }
                }
            }
        } finally {
            lock.writeLock().unlock();
        }
    }

    private <T> Entity<T> read(long pointer) {
        ByteArrayInputStream bais = null;
        ObjectInputStream ois = null;
        try {
            long size = memFile.readLong(pointer);

            byte[] data = memFile.read(pointer + 16, (int) (size - 16));

            bais = new ByteArrayInputStream(data);

            int keyLen = (bais.read() << 8) | bais.read();
            byte[] keyData = new byte[keyLen];
            int readed = 0;
            while (readed < keyLen) {
                readed += bais.read(keyData, readed, keyData.length - readed);
            }
            String key = new String(keyData, "UTF-8");

            ois = new ObjectInputStream(bais);
            Object resO = ois.readObject();

            return new Entity<T>(key, (T) resO);
        } catch (ClassNotFoundException ex) {
            throw new RuntimeException(ex);
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        } finally {
            if (ois != null) {
                try {
                    ois.close();
                } catch (IOException ex) {
                    // who cares
                }
            }
            if (bais != null) {
                try {
                    bais.close();
                } catch (IOException ex) {
                    // who cares
                }
            }
        }
    }

    /**
     * Reads the value from the database which is associated with the given key.
     *
     * @param key The key of the data what you want to read.
     *
     * @return Returns the value which is associated with the given key, or null if the key isn't present in the
     * database.
     */
    public <T> T read(String key) {
        try {
            lock.readLock().lock();
            if (key == null) {
                throw new NullPointerException("The key can not be null.");
            }

            Long pointer = keyCache.get(key);
            if (pointer == null) {
                return null;
            }

            return (T) read(pointer).getValue();
        } finally {
            lock.readLock().unlock();
        }
    }

    private boolean delete(long pointer) {
        long prev = spaceManager.getPrevious(pointer - 11);
        if (prev > -1) {
            prev += 11;
        }
        long next = spaceManager.getNext(pointer - 11);
        if (next > -1) {
            next += 11;
        }

        spaceManager.delete(pointer - 11);

        if (prev > -1) {
            memFile.writeLong(prev + 8, next);

            if (next < 0) {
                if (memFile.length() > spaceManager.getNeededSize() + 2000000) {
                    memFile.setLength(spaceManager.getNeededSize() + 1000000);
                }
            }
        } else {
            memFile.writeLong(3, next);

            if (spaceManager.getNeededSize() < 1) {
                memFile.setLength(100000);

            }
        }
        return true;
    }

    /**
     * Removes the data from the database which associated with the given key.
     *
     * @param key The key of the data which you want to delete.
     *
     * @return True if the data is deleted, or false if the given key isn't in the database.
     */
    public boolean delete(String key) {
        try {
            lock.writeLock().lock();
            if (key == null) {
                throw new NullPointerException("The key can not be null.");
            }

            Long pointer = keyCache.get(key);
            if (pointer == null) {
                return false;
            }

            return delete(pointer);
        } finally {
            lock.writeLock().unlock();
        }
    }

    /**
     * Tells if the given key is presented in the database or not.
     *
     * @param key The key of your interest.
     *
     * @return True if this key is presented in the database, otherwise false.
     */
    public boolean hasKey(String key) {
        return keyCache.containsKey(key);
    }
    
	/**
	 * Checks if there is any key in the database which matches to the given regexp.
	 * 
	 * @param regexp
	 * The regexp.
	 * 
	 * @return 
	 * True if there is one or more key in the database which matches for the given regexp, otherwise false.
	 */
    public boolean matchKey(String regexp) {
        return matchKey(Pattern.compile(regexp));
    }
    
	/**
	 * Checks if there is any key in the database which matches to the given pattern.
	 * 
	 * @param pattern
	 * The regexp pattern.
	 * 
	 * @return 
	 * True if there is one or more key in the database which matches for the given pattern, otherwise false.
	 */
    public boolean matchKey(Pattern pattern) {
        try {
            lock.readLock().lock();
            if (pattern == null) {
                throw new NullPointerException("The pattern can not be null");
            }

            Iterator<Entry<String, Long>> iter = keyCache.entrySet().iterator();
            Entry<String, Long> actEntry;
            Matcher matcher;
            while (iter.hasNext()) {
                actEntry = iter.next();
                matcher = pattern.matcher(actEntry.getKey());
                if (matcher.matches()) {
                    return true;
                }
            }

            return false;
        } finally {
            lock.readLock().unlock();
        }
    }
    
	/**
	 * Returns the count of the keys which matches for the given regexp.
	 * 
	 * @param regexp
	 * The regexp.
	 * 
	 * @return 
	 * The count of the keys which matches for the given regexp.
	 */
    public long keyCount(String regexp) {
        return keyCount(Pattern.compile(regexp));
    }
    
	/**
	 * Returns the count of the keys which matches for the given pattern.
	 * 
	 * @param pattern
	 * The regexp.
	 * 
	 * @return 
	 * The count of the keys which matches for the given pattern.
	 */
    public long keyCount(Pattern pattern) {
        try {
            lock.readLock().lock();
            if (pattern == null) {
                throw new NullPointerException("The pattern can not be null");
            }

            long res = 0;
            Iterator<Entry<String, Long>> iter = keyCache.entrySet().iterator();
            Entry<String, Long> actEntry;
            Matcher matcher;
            while (iter.hasNext()) {
                actEntry = iter.next();
                matcher = pattern.matcher(actEntry.getKey());
                if (matcher.matches()) {
                    res++;
                }
            }

            return res;
        } finally {
            lock.readLock().unlock();
        }
    }
    
    
    
	/**
	 * Returns an Entity which matches to the given regexp and has the same Class Type than the T type parameter.
	 * The ordering of the elements are undefined. It is possible that you can get different results from teh same call.
	 * 
	 * @param <T>
	 * The type of the result.
	 * @param regexp
	 * The regexp.
	 * 
	 * @return 
	 * An Entity which matches to the given regexp and has the same Class Type than the T type parameter or null if no matches can be found.
	 */
    public <T> Entity<T> getEntity(String regexp) {
        return getEntity(Pattern.compile(regexp));
    }
    
	/**
	 * Returns an Entity which matches to the given pattern and has the same Class Type than the T type parameter.
	 * The ordering of the elements are undefined. It is possible that you can get different results from teh same call.
	 * 
	 * @param <T>
	 * The type of the result.
	 * @param pattern
	 * The pattern.
	 * 
	 * @return 
	 * An Entity which matches to the given regexp and has the same Class Type than the T type parameter or null if no matches can be found.
	 */
    public <T> Entity<T> getEntity(Pattern pattern) {
        try {
            lock.readLock().lock();
            if (pattern == null) {
                throw new NullPointerException("The pattern can not be null");
            }

            Iterator<Entry<String, Long>> iter = keyCache.entrySet().iterator();
            Entry<String, Long> actEntry;
            Matcher matcher;
            while (iter.hasNext()) {
                actEntry = iter.next();
                matcher = pattern.matcher(actEntry.getKey());
                if (matcher.matches()) {
                    try {
                        T value = read(actEntry.getKey());
                        return new Entity(actEntry.getKey(), value);
                    } catch (ClassCastException ex) {
                        // simply this isn't the type of data what the user wants
                    }
                }
            }

            return null;
        } finally {
            lock.readLock().unlock();
        }
    }

	/**
	 * Returns an Entity which matches to the given filter and has the same Class Type than the T type parameter.
	 * The ordering of the elements are undefined. It is possible that you can get different results from teh same call.
	 * <p>
	 * This method first calls matchKey() method and the matchValue() method only called if the matchKey() returned true earlyer.
	 * </p>
	 * 
	 * @param <T>
	 * The type of the result.
	 * @param filter
	 * The filter.
	 * 
	 * @return 
	 * An Entity which matches to the given filter and has the same Class Type than the T type parameter or null if no matches can be found.
	 */
    public <T> Entity<T> getEntity(IFilter filter) {
        try {
            lock.readLock().lock();

            Iterator<Entry<String, Long>> iter = keyCache.entrySet().iterator();
            Entry<String, Long> actEntry;
            while (iter.hasNext()) {
                actEntry = iter.next();
                if (filter.matchKey(actEntry.getKey())) {
                    try {
                        T value = read(actEntry.getKey());
                        if (filter.matchValue(actEntry.getKey(), value)) {
                            return new Entity<T>(actEntry.getKey(), value);
                        }
                    } catch (ClassCastException ex) {
                        // simply this isn't the type of data what the user wants
                    }
                }
            }

            return null;
        } finally {
            lock.readLock().unlock();
        }
    }

    public <T> List<Entity<T>> getEntities(IFilter filter, int limit) {
        return getEntities(filter, limit, 0);
    }

    public <T> List<Entity<T>> getEntities(IFilter filter, int limit, int offset) {
        try {
            lock.readLock().lock();
            ArrayList<Entity<T>> res = new ArrayList<Entity<T>>();
            if (limit == 0) {
                return res;
            }

            Iterator<Entry<String, Long>> iter = keyCache.entrySet().iterator();
            Entry<String, Long> actEntry;
            int resultIndex = -1;
            int resultCount = 0;
            while (iter.hasNext()) {
                actEntry = iter.next();
                if (filter.matchKey(actEntry.getKey())) {
                    try {
                        T value = read(actEntry.getKey());
                        if (filter.matchValue(actEntry.getKey(), value)) {
                            resultIndex++;
                            resultCount++;
                            if (resultIndex >= offset) {
                                res.add(new Entity<T>(actEntry.getKey(), value));
                            }
                            if (limit > -1) {
                                if (res.size() >= limit) {
                                    break;
                                }
                            }
                        }
                    } catch (ClassCastException ex) {
                        // simply this isn't the type of data what the user wants
                    }
                }
            }

            return res;
        } finally {
            lock.readLock().unlock();
        }
    }

    public <T> List<Entity<T>> getEntities(String regexp, int limit) {
        return getEntities(Pattern.compile(regexp), limit, 0);
    }

    public <T> List<Entity<T>> getEntities(String regexp, int limit, int offset) {
        return getEntities(Pattern.compile(regexp), limit, offset);
    }

    public <T> List<Entity<T>> getEntities(Pattern pattern, int limit, int offset) {
        try {
            lock.readLock().lock();
            if (pattern == null) {
                throw new NullPointerException("The pattern can not be null");
            }

            ArrayList<Entity<T>> res = new ArrayList<Entity<T>>();
            if (limit == 0) {
                return res;
            }

            Iterator<Entry<String, Long>> iter = keyCache.entrySet().iterator();
            Entry<String, Long> actEntry;
            Matcher matcher;
            int resultIndex = -1;
            int resultCount = 0;
            while (iter.hasNext()) {
                actEntry = iter.next();
                matcher = pattern.matcher(actEntry.getKey());
                if (matcher.matches()) {
                    try {
                        T value = read(actEntry.getKey());
                        resultIndex++;
                        resultCount++;
                        if (resultIndex >= offset) {
                            res.add(new Entity<T>(actEntry.getKey(), value));
                        }
                        if (limit > -1) {
                            if (res.size() >= limit) {
                                break;
                            }
                        }
                    } catch (ClassCastException ex) {
                        // simply this isn't the type of data what the user wants
                    }
                }
            }

            return res;
        } finally {
            lock.readLock().unlock();
        }
    }

    
    
    public <T> T getObject(String regexp) {
        return getObject(Pattern.compile(regexp));
    }
    
    public <T> T getObject(Pattern pattern) {
        try {
            lock.readLock().lock();
            if (pattern == null) {
                throw new NullPointerException("The pattern can not be null");
            }

            Iterator<Entry<String, Long>> iter = keyCache.entrySet().iterator();
            Entry<String, Long> actEntry;
            Matcher matcher;
            while (iter.hasNext()) {
                actEntry = iter.next();
                matcher = pattern.matcher(actEntry.getKey());
                if (matcher.matches()) {
                    try {
                        T value = read(actEntry.getKey());
                        return value;
                    } catch (ClassCastException ex) {
                        // simply this isn't the type of data what the user wants
                    }
                }
            }

            return null;
        } finally {
            lock.readLock().unlock();
        }
    }
    
    public <T> T getObject(IFilter filter) {
        try {
            lock.readLock().lock();

            Iterator<Entry<String, Long>> iter = keyCache.entrySet().iterator();
            Entry<String, Long> actEntry;
            while (iter.hasNext()) {
                actEntry = iter.next();
                if (filter.matchKey(actEntry.getKey())) {
                    try {
                        T value = read(actEntry.getKey());
                        if (filter.matchValue(actEntry.getKey(), value)) {
                            return value;
                        }
                    } catch (ClassCastException ex) {
                        // simply this isn't the type of data what the user wants
                    }
                }
            }

            return null;
        } finally {
            lock.readLock().unlock();
        }
    }

    public <T> List<T> getObjects(IFilter filter, int limit) {
        return getObjects(filter, limit, 0);
    }

    public <T> List<T> getObjects(IFilter filter, int limit, int offset) {
        try {
            lock.readLock().lock();
            ArrayList<T> res = new ArrayList<T>();
            if (limit == 0) {
                return res;
            }

            Iterator<Entry<String, Long>> iter = keyCache.entrySet().iterator();
            Entry<String, Long> actEntry;
            int resultIndex = -1;
            int resultCount = 0;
            while (iter.hasNext()) {
                actEntry = iter.next();
                if (filter.matchKey(actEntry.getKey())) {
                    try {
                        T value = read(actEntry.getKey());
                        if (filter.matchValue(actEntry.getKey(), value)) {
                            resultIndex++;
                            resultCount++;
                            if (resultIndex >= offset) {
                                res.add(value);
                            }
                            if (limit > -1) {
                                if (res.size() >= limit) {
                                    break;
                                }
                            }
                        }
                    } catch (ClassCastException ex) {
                        // simply this isn't the type of data what the user wants
                    }
                }
            }

            return res;
        } finally {
            lock.readLock().unlock();
        }
    }

    public <T> List<T> getObjects(String regexp, int limit) {
        return getObjects(Pattern.compile(regexp), limit, 0);
    }

    public <T> List<T> getObjects(String regexp, int limit, int offset) {
        return getObjects(Pattern.compile(regexp), limit, offset);
    }

    public <T> List<T> getObjects(Pattern pattern, int limit, int offset) {
        try {
            lock.readLock().lock();
            if (pattern == null) {
                throw new NullPointerException("The pattern can not be null");
            }

            ArrayList<T> res = new ArrayList<T>();
            if (limit == 0) {
                return res;
            }

            Iterator<Entry<String, Long>> iter = keyCache.entrySet().iterator();
            Entry<String, Long> actEntry;
            Matcher matcher;
            int resultIndex = -1;
            int resultCount = 0;
            while (iter.hasNext()) {
                actEntry = iter.next();
                matcher = pattern.matcher(actEntry.getKey());
                if (matcher.matches()) {
                    try {
                        T value = read(actEntry.getKey());
                        resultIndex++;
                        resultCount++;
                        if (resultIndex >= offset) {
                            res.add(value);
                        }
                        if (limit > -1) {
                            if (res.size() >= limit) {
                                break;
                            }
                        }
                    } catch (ClassCastException ex) {
                        // simply this isn't the type of data what the user wants
                    }
                }
            }

            return res;
        } finally {
            lock.readLock().unlock();
        }
    }

    /**
     * Closes this database and block while all the pending file I/O operations will be done. Do not interrupt this
     * close operation before it returns normally because it can broke your database file.
     *
     * @throws IOException If an I/O exception is occured throught the closing operation.
     */
    public void close() throws IOException {
        try {
            lock.writeLock().lock();
            if (memFile != null) {
                memFile.close();
                memFile = null;
            }
            keyCache.clear();
        } finally {
            lock.writeLock().unlock();
        }
    }

    @Override
    protected void finalize() throws Throwable {
        try {
            close();
        } catch (IOException iOException) {
            // nothing to do here
        }
        super.finalize();
    }

    private void init() throws IOException {
        if (memFile.length() < 1) {
            memFile.write(0, new byte[]{(byte) 0xad, (byte) 0xa3, (byte) 0xdb});
            memFile.writeLong(3, -1);
        } else if (memFile.length() < 11) {
            throw new IllegalArgumentException("The given file is not a JadarDB file.");
        } else {
            int h1 = memFile.read(0);
            int h2 = memFile.read(1);
            int h3 = memFile.read(2);
            if (h1 != 0xad || h2 != 0xa3 || h3 != 0xdb) {
                throw new IllegalArgumentException("The given file is not a JadarDB file.");
            }

            long len = memFile.length();
            if (len >= 11) {
                long next = memFile.readLong(3);
                if (next < 0) {
                    return;
                }
                long size;
                String key;
                long pointer;
                while (next > 0) {
                    pointer = next;
                    size = memFile.readLong(pointer);
                    next = memFile.readLong(pointer + 8);

                    int keyLen = (memFile.read(pointer + 16) << 8) | memFile.read(pointer + 17);
                    byte[] keyData = memFile.read(pointer + 18, keyLen);
                    key = new String(keyData, "UTF-8");

                    keyCache.put(key, pointer);
                    spaceManager.initSpace(pointer - 11, size);
                }
            } else {
                throw new IllegalArgumentException("The given file is not a JadarDB file.");
            }
        }
    }

    private void shrinkDatabase() {
        Space firstSpace = spaceManager.getFirstSpace();
        if (firstSpace == null) {
            return;
        }

        long nextPointer = spaceManager.getNext(firstSpace.getPointer());
        if (nextPointer > -1) {
            nextPointer += 11;
        } else {
            return;
        }

        Entity e = read(nextPointer);
        delete(nextPointer);
        keyCache.remove(e.getKey());
        write(e.getKey(), e.getValue());
    }

    /**
     * Returns the size of the pending file write tasks.
     *
     * @return The size of the pending file write tasks.
     */
    public int getFileWriteWaitingListSize() {
        return memFile.getWaitingListSize();
    }
}
